﻿using System.Collections.Generic;
using UnityEngine;

namespace Components.PrefabPool
{
    public struct PrefabPoolSetting
    {
        public int InitNums;
        public int KeepNums;
        public int MaxNums;
        public int NewPerFrame;
        public int DeletePerFrame;

        public PrefabPoolSetting(int initNums, int keepNums = 3, int maxNums = 5, int newPerFrame = 1, int deletePerFrame = 1)
        {
            InitNums = initNums;
            KeepNums = keepNums;
            MaxNums = maxNums;
            NewPerFrame = newPerFrame;
            DeletePerFrame = deletePerFrame;
        }
    }
    
    public class PrefabPool
    {
        private readonly Queue<GameObject> _objects = new Queue<GameObject>();
        private readonly GameObject _prototype;
        private readonly int _poolId;
        private readonly Transform _root;

        public PrefabPoolSetting Setting;
        
        internal PrefabPool(GameObject prefab, Transform root, ref PrefabPoolSetting setting)
        {
            Debug.Assert(setting.InitNums <= setting.KeepNums, "initNums must <= keepNums");
            Debug.Assert(setting.KeepNums <= setting.MaxNums, "keepNums must <= maxNums");
            
            _poolId = prefab.GetInstanceID();
            
            var o = new GameObject(prefab.name);
            o.SetActive(false);
            _root = o.transform;
            _root.SetParent(root);
            
            _prototype = CreateProtoType(_poolId, prefab, _root);

            Setting = setting;
            SetupPool();
        }

        public GameObject Get()
        {
            GameObject o;
            if (_objects.Count > 0)
            {
                o = _objects.Dequeue();
                o.transform.SetParent(null);
                o.SetActive(true);
                return o;
            }

            o = Object.Instantiate(_prototype);
            o.SetActive(true);
            return o;
        }

        public void Release(GameObject o)
        {
            var prefabItem = o.GetComponent<PrefabItem>();
            if (prefabItem == null || prefabItem.poolId != _poolId)
            {
                Debug.LogWarning("this gameObject is not belong this pool.");
                Object.Destroy(o);
                return;
            }
  
            o.transform.SetParent(_root);
            _objects.Enqueue(o);
        }

        internal void Tick()
        {
            for (var i = 0; i < Setting.NewPerFrame; i++)
            {
                if (_objects.Count >= Setting.KeepNums)
                    break;
                var o = Object.Instantiate(_prototype, _root, true);
                _objects.Enqueue(o);
            }

            for (var i = 0; i < Setting.DeletePerFrame; i++)
            {
                if (_objects.Count <= Setting.MaxNums)
                    break;
                var o = _objects.Dequeue();
                Object.Destroy(o);
            }
        }

        internal void Clean()
        {
            Object.Destroy(_root);
        }

        private void SetupPool()
        {
            for (var i = 0; i < Setting.InitNums; i++)
            {
                var o = Object.Instantiate(_prototype, _root, false);
                _objects.Enqueue(o);
            }
        }
        
        private static GameObject CreateProtoType(int id, GameObject prefab, Transform root)
        {
            var prototype = Object.Instantiate(prefab, root, false);
            prototype.name = prefab.name;
            var prefabItem = prototype.AddComponent<PrefabItem>();
            prefabItem.poolId = id;
            return prototype;
        }
    }
}